本系列文已出版成書「NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式」,感謝 iT 邦幫忙與博碩文化的協助。如果對 NestJS 有興趣、覺得這個系列文對你有幫助的話,歡迎前往購書,你的支持是我最大的寫作動力!
前一篇有提到如果遇到物件格式的資料要如何做驗證這個問題,事實上這個解法只需要使用 DTO、ValidationPipe
、class-validator 以及 class-transformer ,這裡先完成簡單的前置作業,透過 npm
安裝 class-validator
與 class-transformer
:
$ npm install --save class-validator class-transformer
為了模擬驗證機制,這裡先產生一個 TodoModule
與 TodoController
:
$ nest generate module features/todo
$ nest generate controller features/todo
接著,在 features/todo
下新增 dto
資料夾,並建立 create-todo.dto.ts
:
在驗證格式機制上,必須要採用 class
的形式建立 DTO,原因在Controller(下)這篇有提過,如果採用 interface
的方式在編譯成 JavaScript 時會被刪除,如此一來,Nest 便無法得知 DTO 的格式為何。這裡我們先簡單定義一下 create-todo.dto.ts
的內容:
export class CreateTodoDto {
public readonly title: string;
public readonly description?: string;
}
我希望 title
的規則如下:
String
description
的規則如下:
String
那要如何套用這些規則呢?非常簡單,透過 class-validator
就能辦到,主要是替這些屬性添加特定的裝飾器:
import { IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
export class CreateTodoDto {
@MaxLength(20)
@IsString()
@IsNotEmpty()
public readonly title: string;
@IsString()
@IsOptional()
public readonly description?: string;
}
提醒:詳細的裝飾器內容可以參考 class-validator。
如此一來便完成了規則的定義,實在是太好用啦!接下來只需要在資源上透過 @UsePipes
裝飾器套用 ValidationPipe
即可:
import { Body, Controller, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
@Controller('todos')
export class TodoController {
@Post()
@UsePipes(ValidationPipe)
create(@Body() dto: CreateTodoDto) {
return {
id: 1,
...dto
};
}
}
在 Controller 層級套用也可以,就會變成該 Controller 下的所有資源都支援驗證:
import { Body, Controller, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
@Controller('todos')
@UsePipes(ValidationPipe)
export class TodoController {
@Post()
create(@Body() dto: CreateTodoDto) {
return {
id: 1,
...dto
};
}
}
透過 Postman 來測試,會發現順利報錯:
如果不想要回傳錯誤的項目,可以透過 ValidationPipe
的 disableErrorMessages
來關閉:
import { Body, Controller, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
@Controller('todos')
export class TodoController {
@Post()
@UsePipes(new ValidationPipe({ disableErrorMessages: true }))
create(@Body() dto: CreateTodoDto) {
return {
id: 1,
...dto
};
}
}
透過 Postman 進行測試:
與其他 Pipe 一樣可以透過 exceptionFactory
自訂 Exception:
import { Body, Controller, HttpStatus, NotAcceptableException, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { ValidationError } from 'class-validator';
import { CreateTodoDto } from './dto/create-todo.dto';
@Controller('todos')
export class TodoController {
@Post()
@UsePipes(
new ValidationPipe({
exceptionFactory: (errors: ValidationError[]) => {
return new NotAcceptableException({
code: HttpStatus.NOT_ACCEPTABLE,
message: '格式錯誤',
errors
});
}
})
)
create(@Body() dto: CreateTodoDto) {
return {
id: 1,
...dto
};
}
}
透過 Postman 進行測試:
以前面新增 Todo 的例子來說,可接受的參數為 title
與 description
,假設今天客戶端傳送下方資訊:
{
"title": "Test",
"text": "Hello."
}
可以發現傳了一個毫無關聯的 text
,這時候想要快速過濾掉這種無效參數該怎麼做呢?透過 ValidationPipe
設置 whitelist
即可,當 whitelist
為 true
時,會 自動過濾掉於 DTO 沒有任何裝飾器的屬性,也就是說,就算有該屬性但沒有添加 class-validator
的裝飾器也會被視為無效屬性。這裡我們簡單實驗一下 whitelist
:
import { Body, Controller, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
@Controller('todos')
export class TodoController {
@Post()
@UsePipes(new ValidationPipe({ whitelist: true }))
create(@Body() dto: CreateTodoDto) {
return {
id: 1,
...dto
};
}
}
透過 Postman 進行測試:
如果想要傳送無效參數時直接報錯的話,則是同時使用 whitelist
與 forbidNonWhitelisted
:
import { Body, Controller, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
@Controller('todos')
export class TodoController {
@Post()
@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
create(@Body() dto: CreateTodoDto) {
return {
id: 1,
...dto
};
}
}
透過 Postman 進行測試:
ValidationPipe
還提供 transform
參數來轉換傳入的物件,將其實例化為對應的 DTO:
import { Body, Controller, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
@Controller('todos')
export class TodoController {
@Post()
@UsePipes(new ValidationPipe({ transform: true }))
create(@Body() dto: CreateTodoDto) {
console.log(dto);
return dto;
}
}
透過 Postman 進行測試,會在終端機看到下方結果,會發現 dto
為 CreateTodoDto
實例:
CreateTodoDto { title: 'Test' }
transform
還有一個很厲害的功能,還記得如何取得路由參數嗎?假設路由參數要取得 id
,這個 id
型別是 number
,但正常來說路由參數收到的時候都會是 string
,透過 transform
Nest 會嘗試去轉換成我們指定的型別:
import { Controller, Get, Param, UsePipes, ValidationPipe } from '@nestjs/common';
@Controller('todos')
export class TodoController {
@Get(':id')
@UsePipes(new ValidationPipe({ transform: true }))
get(@Param('id')id : number) {
console.log(typeof id);
return '';
}
}
透過瀏覽器存取 http://localhost:3000/1,會在終端機看到型別確實轉換成 number
了:
number
如果傳入的物件為陣列格式,不能使用 ValidationPipe
,要使用 ParseArrayPipe
,並在 items
帶入其 DTO:
import { Body, Controller, ParseArrayPipe, Post } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
@Controller('todos')
export class TodoController {
@Post()
create(
@Body(new ParseArrayPipe({ items: CreateTodoDto }))
dtos: CreateTodoDto[]
) {
return dtos;
}
}
透過 Postman 進行測試:
ParseArrayPipe
還可以用來解析查詢參數,假設查詢參數為 ?ids=1,2,3
,此時就可以善用此方法來解析出各個 id
,只需要添加 separator
去判斷以什麼作為分界點:
import { Controller, Get, ParseArrayPipe, Query } from '@nestjs/common';
@Controller('todos')
export class TodoController {
@Get()
get(
@Query('ids', new ParseArrayPipe({ items: Number, separator: ',' }))
ids: number[]
) {
return ids;
}
}
透過 Postman 進行測試:
當系統越來越龐大的時候,DTO 的數量也會隨之增加,有許多的 DTO 會有重複的屬性,例如:相同資源下的 CRUD DTO,這時候就會變得較難維護,還好 Nest 有提供良好的解決方案,運用特殊的繼承方式來處理:
局部性套用的意思是將既有的 DTO 所有欄位都取用,只是全部轉換為非必要屬性,需要使用到 PartialType
這個函式來把要取用的 DTO 帶進去,並給新的 DTO 繼承。這邊我們先建立一個 update-todo.dto.ts
在 dto
資料夾中,並讓它繼承 CreateTodoDto
的欄位:
import { PartialType } from '@nestjs/mapped-types';
import { CreateTodoDto } from './create-todo.dto';
export class UpdateTodoDto extends PartialType(CreateTodoDto) {
}
其效果相當於:
import { IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
export class UpdateTodoDto {
@MaxLength(20)
@IsString()
@IsNotEmpty()
@IsOptional()
public readonly title?: string;
@IsString()
@IsOptional()
public readonly description?: string;
}
接著來修改 todo.controller.ts
:
import { Body, Controller, Param, Patch, UsePipes, ValidationPipe } from '@nestjs/common';
import { UpdateTodoDto } from './dto/update-todo.dto';
@Controller('todos')
export class TodoController {
@Patch(':id')
@UsePipes(ValidationPipe)
update(
@Param('id') id: number,
@Body() dto: UpdateTodoDto
) {
return {
id,
...dto
};
}
}
透過 Postman 進行測試,這邊我不帶任何值去存取 PATCH /todos/:id
,會發現可以通過驗證:
選擇性套用的意思是用既有的 DTO 去選擇哪些是會用到的屬性,需要使用到 PickType
這個函式來把要取用的 DTO 帶進去以及指定要用的屬性名稱,並給新的 DTO 繼承。這邊我們沿用 UpdateTodoDto
並讓它繼承 CreateTodoDto
的 title
欄位:
import { PickType } from '@nestjs/mapped-types';
import { CreateTodoDto } from './create-todo.dto';
export class UpdateTodoDto extends PickType(CreateTodoDto, ['title']) {
}
其效果等同於:
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';
export class UpdateTodoDto {
@MaxLength(20)
@IsString()
@IsNotEmpty()
public readonly title: string;
}
todo.controller.ts
沿用前面的範例:
import { Body, Controller, Param, Patch, UsePipes, ValidationPipe } from '@nestjs/common';
import { UpdateTodoDto } from './dto/update-todo.dto';
@Controller('todos')
export class TodoController {
@Patch(':id')
@UsePipes(ValidationPipe)
update(
@Param('id') id: number,
@Body() dto: UpdateTodoDto
) {
return {
id,
...dto
};
}
}
透過 Postman 進行測試,這邊我不帶任何值去存取 PATCH /todos/:id
,會發現無法通過驗證:
忽略套用的意思是用既有的 DTO 但忽略不會用到的屬性,需要使用到 OmitType
這個函式來把要取用的 DTO 帶進去以及指定要忽略的屬性名稱,並給新的 DTO 繼承。這邊我們沿用 UpdateTodoDto
並讓它繼承 CreateTodoDto
的欄位,但忽略 title
屬性:
import { OmitType } from '@nestjs/mapped-types';
import { CreateTodoDto } from './create-todo.dto';
export class UpdateTodoDto extends OmitType(CreateTodoDto, ['title']) {
}
其效果等同於:
import { IsOptional, IsString } from 'class-validator';
export class UpdateTodoDto {
@IsString()
@IsOptional()
public readonly description?: string;
}
這裡稍微調整一下 todo.controller.ts
,將 whitelist
與 forbidNonWhitelisted
設為 true
:
import { Body, Controller, Param, Patch, UsePipes, ValidationPipe } from '@nestjs/common';
import { UpdateTodoDto } from './dto/update-todo.dto';
@Controller('todos')
export class TodoController {
@Patch(':id')
@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
update(
@Param('id') id: number,
@Body() dto: UpdateTodoDto
) {
return {
id,
...dto
};
}
}
透過 Postman 進行測試,這邊我刻意帶 title
去存取 PATCH /todos/:id
,由於設置了 whitelist
與 forbidNonWhitelisted
,所以無法通過驗證:
合併套用的意思是用既有的兩個 DTO 來合併屬性,需要使用到 IntersectionType
這個函式來把要取用的兩個 DTO 帶進去,並給新的 DTO 繼承。這邊我們沿用 CreateTodoDto
並在 update-todo.dto.ts
新增一個 MockDto
,再讓 UpdateTodoDto
去繼承這兩個的欄位:
import { IntersectionType } from '@nestjs/mapped-types';
import { IsNotEmpty, IsString } from 'class-validator';
import { CreateTodoDto } from './create-todo.dto';
export class MockDto {
@IsString()
@IsNotEmpty()
public readonly information: string;
}
export class UpdateTodoDto extends IntersectionType(CreateTodoDto, MockDto) {
}
其效果等同於:
import { IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
export class UpdateTodoDto {
@MaxLength(20)
@IsString()
@IsNotEmpty()
public readonly title: string;
@IsString()
@IsOptional()
public readonly description?: string;
@IsString()
@IsNotEmpty()
public readonly information: string;
}
這裡調整一下 todo.controller.ts
:
import { Body, Controller, Param, Patch, UsePipes, ValidationPipe } from '@nestjs/common';
import { UpdateTodoDto } from './dto/update-todo.dto';
@Controller('todos')
export class TodoController {
@Patch(':id')
@UsePipes(ValidationPipe)
update(
@Param('id') id: number,
@Body() dto: UpdateTodoDto
) {
return {
id,
...dto
};
}
}
透過 Postman 進行測試,這邊我刻意不帶 information
去存取 PATCH /todos/:id
,所以無法通過驗證:
上述的四個函式:PartialType
、PickType
、OmitType
、IntersectionType
是可以透過組合的方式來使用的。下方的範例使用 OmitType
將 CreateTodoDto
的 title
欄位去除,並使用 IntersectionType
把 MockDto
與之合併 :
import { IntersectionType, OmitType } from '@nestjs/mapped-types';
import { IsNotEmpty, IsString } from 'class-validator';
import { CreateTodoDto } from './create-todo.dto';
export class MockDto {
@IsString()
@IsNotEmpty()
public readonly information: string;
}
export class UpdateTodoDto extends IntersectionType(
OmitType(CreateTodoDto, ['title']), MockDto
) {
}
其效果等同於:
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class UpdateTodoDto {
@IsString()
@IsOptional()
public readonly description?: string;
@IsString()
@IsNotEmpty()
public readonly information: string;
}
todo.controller.ts
保持本來的樣子:
import { Body, Controller, Param, Patch, UsePipes, ValidationPipe } from '@nestjs/common';
import { UpdateTodoDto } from './dto/update-todo.dto';
@Controller('todos')
export class TodoController {
@Patch(':id')
@UsePipes(ValidationPipe)
update(
@Param('id') id: number,
@Body() dto: UpdateTodoDto
) {
return {
id,
...dto
};
}
}
透過 Postman 進行測試,這邊我不帶任何值去存取 PATCH /todos/:id
,會發現無法通過驗證:
ValidationPipe
算是一個蠻常用的功能,因為大多數的情況都會使用到 DTO 的概念,如此一來便可以使用 DTO 驗證的方式去檢查資料的正確性,所以可以直接將 ValidationPipe
配置在全域,僅需要修改 main.ts
即可:
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
透過 useGlobalPipes 使 ValidationPipe
適用於全域,實在是非常方便!
上面的方法是透過模組外部完成全域配置的,與 Exception filter 一樣可以用依賴注入的方式,透過指定 Provider 的 token
為 APP_PIPE
來實現,這裡是用 useClass 來指定要建立實例的類別:
import { Module, ValidationPipe } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoModule } from './features/todo/todo.module';
@Module({
imports: [TodoModule],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_PIPE,
useClass: ValidationPipe
}
],
})
export class AppModule {}
ValidationPipe
與 DTO 的驗證機制十分好用且重要,任何的 API 都需要做好完善的資料檢查,才能夠降低帶來的風險。這裡附上今天的懶人包:
ValidationPipe
需要安裝 class-validator
及 class-transformer
。ValidationPipe
可以實現 DTO 格式驗證。ValidationPipe
可以透過 disableErrorMessages
關閉錯誤細項。ValidationPipe
一樣可以透過 exceptionFactory
自訂 Exception。ValidationPipe
可以透過 whitelist
來過濾無效參數,如果接收到無效參數想要回傳錯誤的話,還需要額外啟用 forbidNonWhitelisted
。ValidationPipe
可以透過 transform
來達到自動轉換型別的效果。ParseArrayPipe
解析陣列 DTO 以及查詢參數。PartialType
、PickType
、OmitType
、IntersectionType
這四個函式來重用 DTO 的欄位。非常感谢,在大陆很少看到nestjs适合自己的系列文章,官网翻译又比较生硬,现在轻松入门nestjs了,感激之情,无以言表
能夠幫助大家入門 NestJS 是我的榮幸,謝謝你的讚賞!
您好,想咨询一下,若页面和服务端都是用typescript研发,那DTO的结构是共用的,在与服务端分离的场景下,如何共用DTO呢?
你好,如果說不介意採用 monorepo 架構的話,可以研究一下 Nx。
好的,谢谢,我研究一下。
您好,感謝教學,
想請問,我測試無法在local去更改global設定的值耶,
例如我在global設定transform: true
但在local(Controller)裡面想單獨改成transform: false
是無法成功的,想請問有辦法解決嗎?
因為如果local無法單獨修改,會大大降低global設定的可用性
據我所知,使用全域的方式配置確實無法單獨修改,原因是這個功能會對所有 Controller 底下的 Handler 起作用,而全域 Pipe 的優先級別會大於 Controller 的,所以資料會先被全域 Pipe 給轉換。
了解,感謝!
那可能就global開沒設定值的,
需要設定值再從local設定,
感謝啦!